Setup Decorator
The book has now been published and the content of this chapter has likely changed substanstially.Please see page 447 of xUnit Test Patterns for the latest information.
How do we cause the Shared Fixture to be built before the first test method that needs it?
We wrap the test suite with a Decorator that sets up the shared test fixture before running the tests and tears it down after all the tests are done.
Sketch Setup Decorator embedded from Setup Decorator.gif
If we have chosen to use a Shared Fixture (page X) whether it be for reasons of convenience or necessity and we have chosen not to use a Prebuilt Fixture (page X), we will need to ensure that the fixture gets built before each test run. Lazy Setup (page X) is one strategy we could employ to create the test fixture "just in time" for the first test. But if it is critical to tear down the fixture after the last test, how do we know that all the tests have been completed?
How It Works
Setup Decorator works by "bracketing" the execution of the entire test suite with a set of matching setUp and tearDown "book ends". The pattern Decorator[GOF] is just what we need to make this happen. We construct a Setup Decorator that holds a reference to the Test Suite Object (page X) we wish to decorate and pass it to the Test Runner (page X). When it is time to run the test, the Test Runner calls the run method on our Setup Decorator rather than the one on the actual Test Suite Object. The Setup Decorator does the fixture setup before calling the run method on the Test Suite Object and tears down the fixture after it returns.
When To Use It
We can use a Setup Decorator when it is critical that a Shared Fixture be set up before every test run and that it is torn down after the run is complete. It may be critical because tests are using Hard-Coded Values (see Literal Value on page X) that would cause the tests to fail if run again without cleaning up after each run (Unrepeatable Tests (see Erratic Test on page X)) or it may just be the issue of the database slowly filling up with data from repeated test runs.
Another reason for using a Setup Decorator is when the tests need to change some global parameter before exercising the system under test (SUT) and they need to change it back when they are done. Replacing the database with a Fake Database (see Fake Object on page X) to avoid Slow Tests (page X) is one common reason for doing this; setting global switches to a particular configuration is another. Setup Decorators are installed at runtime so there is nothing stopping us from using several different decorators on the same test suite at different times (or even the same time.)
As an alternative to Setup Decorator we can use SuiteFixture Setup (page X) if we only want to share the fixture across the tests in a single Testcase Class (page X) and our member of the xUnit family support it. If it is not essential to have the fixture torn down after every test run, we could use Lazy Setup instead.
Implementation Notes
A self consists of an object that sets up the fixture, delegates test execution to the test suit to be run, and then executes the code to tear down the fixture. To better line up with the normal XUnit calling conventions, we typically put the code that constructs the test fixture into a method called setUp and the code the tears down the fixture into a method called tearDown. Then our Setup Decorator'srun logic consists of three lines of code:
void run() { setup(); decoratedSuite.run(); teardown(); } Example FixtureSetupDecoratorRunMethod embedded from java/com/clrstream/ex6/services/test/ParameterizedFlightManagementTestSetup.java
There are several ways to build the Setup Decorator.
Variation: Abstract Setup Decorator
Many members of the XUnit family of Test Automation Frameworks (page X) provide a reusable superclass that implements Setup Decorator. This class is usually implements the setUp/run/tearDown sequence as a Template Method[GOF]. All we have to do is subclass this class and implement the setUp and tearDown methods as we would in a normal Testcase Class. When instantiating our Setup Decorator class, we pass the Test Suite Object we are decorating as the constructor argument.
Variation: Hard-Coded Setup Decorator
If we need to build our Setup Decorator from scratch, the "simplest thing that could possibly work" is to hard-code the name of the decorated class in the suite method of the Decorator. This allows the Decorator class to act as the Test Suite Factory (see Test Enumeration on page X) for the decorated suite.
Variation: Parameterized Setup Decorator
If we want to reuse the Setup Decorator for different test suites, we can parameterize the Setup Decorator's constructor method with the Test Suite Object to be run; this means that the setup and tear down logic can be coded within the Setup Decorator eliminating the need for a separate Test Helper (page X) class just to reuse the setup logic across tests.
Variation: Decorated Lazy Setup
One of the main drawbacks of using a Setup Decorator is that that tests cannot be run by themselves because they depend on the Setup Decorator to set up the fixture. We can work around this by augmenting it with Lazy Setup in the setUp method so that an undecorated Testcase Object (page X) can construct it's own fixture. It can remember that it built it's own fixture and destroy it in the tearDown method. This functionality could be implemented on a generic Testcase Superclass (page X) so that it only has to be built and tested once. The only other alternative is to use a Pushdown Decorator but since that would negate any test speedup the Shared Fixture bought us it can only be used it those cases when we use the Setup Decorator for reasons other than setting up a Shared Fixture.
Variation: Pushdown Decorator
One of the main drawbacks of using a Setup Decorator is that that tests cannot be run by themselves because they depend on the Setup Decorator to set up the fixture. One way we have circumvented this drawback is to provide a means to push the decorator down to the level of the individual tests rather than the whole test suite. This required a few modifications to the TestSuite class to allow the Setup Decorator to be passed down to where the individual Testcase Objects are constructed during the Test Discovery (page X) process. As each object is created from the Test Method (page X) it is first wrapped in the Setup Decorator before it is added to the Test Suite Object's collection of tests.
Of course, this negates one of the main sources of a speed advantage of using a Setup Decorator by forcing a new test fixture to be built for each test. In our case, this wasn't a problem because we were using the decorator to replace the real database infrastructure with an in-memory database so we got our speed improvement that way. (See the sidebar Faster Tests Without Shared Fixtures (page X).)
Motivating Example
In this example, we have a set of tests that use Lazy Setup to build the Shared Fixture and Finder Methods (see Test Utility Method on page X) to find the objects in the fixture. We have discovered that the leftover fixture is causing Unrepeatable Tests so we want to clean up properly after the last test has finished running.
protected void setUp() throws Exception { if (sharedFixtureInitialized) { return; } facade = new FlightMgmtFacadeImpl(); setupStandardAirportsAndFlights(); sharedFixtureInitialized = true; } protected void tearDown() throws Exception { // Cannot delete any objects because we don't know // whether or not this is the last test } Example LazyFixtureInitialization embedded from java/com/clrstream/ex6/services/test/SharedFixtureFlightManagementFacadeTest.java
Since there is no easy way to do this with Lazy Setup, we will have to change our fixture setup strategy; one option is to use a Setup Decorator instead.
Refactoring Notes
When creating a Setup Decorator, we can reuse the exact same fixture setup logic; we just need to call it at a different time. So this refactoring consists mostly of moving the call to the fixture setup logic from the setUp method on the Testcase Class to the setUp method of a Setup Decorator class. Assuming we have an Abstract Setup Decorator available to subclass, we create our new subclass and provide a concrete implementation of the setUp and tearDown methods.
If our instance of xUnit does not support Setup Decorator directly, we can create our own Setup Decorator superclass by building a single-purpose Setup Decorator and then introducing a constructor parameter and instance variable to hold the test suite to be run. Finally, we do an Extract Superclass[Fowler] refactoring to create our reusable superclass.
Example: Hard-coded Setup Decorator
In this example, we have moved all the setup logic to the setUp method of a Setup Decorator which inherits its basic functionality from an Abstract Setup Decorator. We have also written some fixture tear down logic in the tearDown method so that we clean up the fixture after the entire test suite has been run.
public class FlightManagementTestSetup extends TestSetup { private FlightManagementTestHelper helper; public FlightManagementTestSetup() { // Construct the Test Suite Object to be decorated and // pass it to our Abstract Setup Decorator superclass. super( SafeFlightManagementFacadeTest.suite() ); helper = new FlightManagementTestHelper(); } public void setUp() throws Exception { helper.setupStandardAirportsAndFlights(); } public void tearDown() throws Exception { helper.removeStandardAirportsAndFlights(); } public static Test suite() { // Return an instance of this decorator class: return new FlightManagementTestSetup(); } } Example HardCodedFixtureSetupDecorator embedded from java/com/clrstream/ex6/services/test/FlightManagementTestSetup.java
Because this is a Hard-Coded Setup Decorator, the call to the Test Suite Factory that builds the actual Test Suite Object is hard-coded inside constructor; the suite method just calls the constructor.
Example: Parameterized Setup Decorator
To make our Setup Decorator reusable with several different test suites, we need to do an Introduce Parameter[JBrains] refactoring on the name of the Test Suite Factory inside the constructor.
public class ParameterizedFlightManagementTestSetup extends TestSetup { private FlightManagementTestHelper helper = new FlightManagementTestHelper(); public ParameterizedFlightManagementTestSetup( Test testSuiteToDecorate) { super(testSuiteToDecorate); } public void setUp() throws Exception { helper.setupStandardAirportsAndFlights(); } public void tearDown() throws Exception { helper.removeStandardAirportsAndFlights(); } } Example ParameterizedFixtureSetupDecorator embedded from java/com/clrstream/ex6/services/test/ParameterizedFlightManagementTestSetup.java
To make it easy for the Test Runner to create our test suite, we will also need to create a Test Suite Factory that calls the Setup Decorator's constructor with the Test Suite Object to be decorated.
public class DecoratedFlightManagementFacadeTestFactory { public static Test suite() { // Return a new Test Suite Object suitably decorated: return new ParameterizedFlightManagementTestSetup( SafeFlightManagementFacadeTest.suite()); } } Example ParameterizedFixtureSetupDecoratorUsage embedded from java/com/clrstream/ex6/services/test/DecoratedFlightManagementFacadeTestFactory.java
We will need one of these for each test suite we want to be able to run by itself but this is a small price to pay for reusing the actual Setup Decorator.
Example: Abstract Decorator Class
Here's what the Abstract Decorator Class looks like:
public class TestSetup extends TestCase { Test decoratedSuite; AbstractSetupDecorator(Test testSuiteToDecorate) { decoratedSuite = testSuiteToDecorate; } public void setUp() throws Exception { // subclass responsibility } public void tearDown() throws Exception { // subclass responsibility } void run() { setup(); decoratedSuite.run(); teardown(); } } Example AbstractFixtureSetupDecorator embedded from java/com/clrstream/ex6/services/test/ParameterizedFlightManagementTestSetup.java
Copyright © 2003-2008 Gerard Meszaros all rights reserved